Repository navigation
Conversation
…s by hash/key/payload, index bytes) in EXPLAIN ANALYZE and PROFILE (X3' step 1) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…; a test of the build memory counters; snapshots (X3' step 1) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…t they cover (review nits) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…nter test asserts the build rows exactly and bounds the hash bytes (review of #586) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com> # Conflicts: # docs/reference/explain-analyze.md
…nation copy at finalize or grace reload; a per-row chunk table for the probe's lookups (X3' step 3, option B) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…ALYZE snapshots show the build counters; grace.rs file-size line Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
… only; max_finalize_copy_bytes dropped); a test that a chunked arena reads and indexes as one batch, past the 256-chunk table too Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
… per 64 rows, in cache) instead of a byte per row; an arena of no rows keeps an empty chunk, so an empty grace build partition still builds; a joined grace partition keeps its build part's reservations (review of X3' step 3) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…w is row id & mask of block id >> 14, with no chunk table or search; build chunks are copied once into blocks as they come, a grace reload blocks its frames as it reads them, finalize slices whole blocks in place and copies only the tails (reserved; the tails beside chunks that live on stay reserved) Study 10 §4: option B's dependent chunk-table load before each row fired the probe_table +20% kill line. The tiered coalesce stays for the probe side; both move to join/block.rs (grace.rs under its file-size line). An arena's arrow key bytes count each block's own range. Opus review ops/reviews/2026-11-07-opus-1-x3-option-c-diff.md, fixes listed. Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…ng build reserves every index and the largest copy, not every copy at once); the blocked-arena test gets a tail beside its blocks Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
… so the copy it holds for one partition at a time funds transient copies only (kept tails no longer use it up and leave a later partition unfunded); a test over chunks with half-block tails under tight limits Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…t reading it (J4, study 15; DuckDB's chains_longer_than_one): the probe and mark_chain skip next() on unique tables, grace tables too Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…1 §4.4, option C's block slack): the chunks were reserved at their allocated size, builder slack and tails charged at the chunk they were sliced from; finalize copies the tails into exact blocks and drops the chunks, then trues each partition's reservation down to the arena's buffers (RowArena::held_bytes, each buffer's capacity counted once) A build of 256-row batches over 40,000 keys kept 3.24 MB reserved for a 2.40 MB arena (+35%); trued up it keeps <= 5% over (test a_built_table_keeps_reserved_what_its_arena_holds). Study 21 measured ~27 MB of such slack per chunked build at 12 threads on q21 SF10 (query peak +17% against main, a spill at 300 MiB where main has none). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…he kept tail bytes it took (the trim leaves them to the chunks' reservation); the test bounds steady from below too; held_bytes sized and documented Review: ops/reviews/2026-11-08-opus-1-x3-arena-trueup-diff.md (Opus, approve with nits). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…keys, at load up to 4/5 (study 21 §5-6, S5 lever 3; DuckDB's load factor), instead of twice its keys at load up to 1/2 next_pow2(2 x keys) landed at 3.7 slots per key on q21's 145 M-key build at SF1000 (5.1 GB, a third of op 6's held table); at 5/4 it saves 2.5 GB on op 6 and 1.3 GB on op 5 and op 4 there, and nothing at SF10, where the rounding lands on the same power. The 16-bit salt keeps a probe from reading a row for all but 1 in 65,536 of the slots it passes; the probe cost at the higher load is what perf-1's A/B against p2b/opus1-x3-copy-free decides (study 15 measured max_probe_length 2 at load 0.35). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…i joins that never read it (LatePayloadJoin, P2b S5 lever 2; q21's s_name)
TPC-H q21 joins supplier ⋈ nation('SAUDI ARABIA') to l1 first and then
carries s_name (31 of the 79 B of arena a build row) through the join with
orders, the EXISTS semi join and the NOT EXISTS anti join, all built on the
l1 side; only the aggregate on top reads it (study 21 §4.1, §6). The new
rule (late_payload.rs) moves such an inner equi-join to the top of its path
and leaves a key-only semi join in its place:
Consumer(Path(X ⋈[x = d] D))
=> Consumer(Project[out(Path)](Path'(X ⋉[x = d'] Shift(π_d D)) ⋈[x = d] D))
Exact with no uniqueness assumption: every admitted path node (filters,
projections, inner joins either side, left semi/anti/existence/outer joins
with the path on the left, right outer on the right; none reading the
dimension's columns, all deterministic) commutes with an inner join on
columns it does not read, so each X row still meets every matching D row;
the semi join drops only rows the top join drops. D is computed twice (the
semi join's instance under a Shift, as restrict does), so it must be
restrict::repeatable.
Applies only with at least one non-inner join on the path (join ordering
never moves an inner join back across one, so the plan stays a fixed
point), when no other parent reads the join or a path node (no copied
shared subtree), and with statistics on both join sides (never on default
row counts): the carried payload is variable-width or >= 16 B a row, the
dimension estimates no more rows than the other side and scans no more
rows than it holds (it is computed twice), and some join on the path builds
the path's side as the physical planner orients it (an inner join's right
input; the left input of a semi, non-null-aware anti or left outer join
whose right input costs more than 2x and is not broadcast, orient.rs; the
broadcast threshold is the session's). A payload on a probe side only
streams. Runs after Prune/Tidy, so the payload is what the path really
carries; when it moves a join, join order, eager aggregation and the
prune/tidy loop (now prune_and_tidy) run again. Several dimensions under one
consumer all move in one pass. EXPLAIN VERBOSE notes each move and each
refused gate, once each (LatePayloadJoin).
Switch: krishiv.optimizer.latePayloadJoin (session, user, default true),
Options::late_payload_join. Design: krishiv-internal
design/P2b-S5-dimension-payload-late.md.
Plans: TPC-H with the statistics fixture changes q21 (s_name) and q18
(c_name leaves the orders build of the join with lineitem; q18's SF1000
row is the one that spills); TPC-DS with SF1 row counts changes q35, q58,
q72 and q83 (listed in a snapshot); without statistics nothing changes.
Tests: optimizer tests/it/late_payload.rs: q21 and q18 move and nothing
else in TPC-H does; repeating dimension keys (nation on n_regionkey, five
rows a key) under semi, anti and projection consumers and NULL keys; two
dimensions under one consumer; a left outer path; refusals (payload or
dimension key read by the semi join, a nondeterministic dimension, an 8 B
payload, the larger dimension, a broadcast semi side, an existence join, a
right outer join with the path on the right, an all-inner path, default row
counts); the switch by option and config; each checked against the
reference interpreter and for the fixed point; TPC-DS with row counts is a
fixed point. exec tests/planner/late_payload.rs: q21, q18 and two
repeating-key queries at SF0.01 (broadcast off, so the rule fires) return
the rows of the same plan with the rule off. common: the key's default and
parsing.
Review: krishiv-internal ops/reviews/2026-11-09-opus-1-late-payload-diff.md
(Reviewer: Opus; request changes, fixed: the orient-matching build gate,
the executor test against the rule off, TPC-DS with statistics, the missing
refusals and NULL keys, the D cost bound, shared path nodes, the prune/tidy
loop, module order).
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…s note, join_settings keeps its doc; q18's EXPLAIN ANALYZE snapshot Refusal notes are still written once; two moves with the same keys each get their line. The q18 EXPLAIN ANALYZE snapshot (session explain_analyze_tpch) takes the plan the rule gives it (c_name joins above the IN semi join). Review: krishiv-internal ops/reviews/2026-11-09-opus-1-late-payload-diff.md (re-check, Reviewer: Opus, approve with nits). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
… joins above the IN semi join) Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…arena block by block; main's tiered chunk merge lives in block.rs Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…stack: the rule re-wired into main's pipeline (prune_and_tidy, eager_pass), its config entry in the split registry, q18/q21 snapshots regenerated Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com> # Conflicts: # crates/krishiv-common/src/config/registry.rs # crates/krishiv-common/src/config/tests.rs
…r grows past its reservation salted_slots rounded 5/4 of the key count down. Where n + n/4 lands exactly on a power of two and n is not a multiple of 4 (1,639 keys: 1,639 + 409 = 2,048 slots), the last key failed the load-4/5 check and the directory doubled while being built, past the index bytes reserved for it (grace loads and the finalize true-up both assume it never grows). Now n + ceil(n/4): 5n <= 4 slots always holds. A test builds 1,639 and neighbouring key counts and checks the directory keeps its planned size (it fails on the old sizing). Comments that still assumed load 1/2 say which measurements were taken at it. Opus review of the S5 stack: ops/reviews/2026-11-13-perf-2-s5-stack-diff.md (Important finding). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…nalize of a small build no longer walks thousands Option C replaced the build side's size-tiered coalesce with the block cut, which merges chunks only once a trailing run holds a block's 2^14 rows. A build fed small batches pushes chunks of a few rows (12 drivers cutting each batch into its partitions): TPC-H q10 at SF10 held 29,359 chunks for 573,157 rows and a 1.1M-row build 115,394. Finalize walked every one twice for its reservation (RowArena::reblock_bytes) and then concatenated them, serially before any partition started: finalize took 72-89 ms where main took 9 (perf-1's 11-09 ABBA line: "every small build slower", the open item before option C's PR). Part::block now first merges runs of 64 chunks shorter than a block, size-tiered among themselves (the probe side's coalesce rule, fan-out 64, chunks of a block or more never merged again; chunks big enough to fill a block before 64 are held are still copied once, by the cut). Finalize also computes each partition's reblock bytes once, not twice. Measured on the laptop, SF10 warm PROFILE, 12 threads, finalize wall per build (instrumented scratch build of this code, not committed), option C before -> after: q10's 573K-row build 72-89 -> 14.5-16.3 ms (chunks 29,359 -> about 5,000; main was 9), the 1.1M-row build 310-339 -> 24-26 ms, q09's 435K-row build 50-77 -> 17-23 ms, its 109K-row build 10-11 -> 8-9 ms; the 3.3M-row build (whole blocks) unchanged at 40-42 ms. A test pushes 5,000 chunks of 20 rows and checks fewer than 128 are held, rows in order, the reservation covering them. Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…; deterministicOrder names the build's block copy Re-review M6 of the S5 stack: Part::block merged runs of 64 short chunks before the block cut, so a run that already filled a block (63 chunks of 255 rows, then 400) became one 16,465-row chunk: its rows were copied twice and an 81-row tail stayed beside it. The cut now runs first. A test checks that run gives a block and an 81-row tail. M1: the krishiv.deterministicOrder doc says a hash join build's block copy under memory pressure can change the order of what it returns, as a spill can. Review: ops/reviews/2026-11-14-perf-2-s5-stack-rereview.md (APPROVED). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com> # Conflicts: # crates/krishiv-common/src/config/tests.rs
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will improve performance by 47.47%1 warning
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
block_chunks built a block from the trailing run's chunks up to the one that fills it, then truncated the list at the run's start: every chunk past that one was dropped with it. A cut skipped while its copy could not be reserved, then more chunks pushed (a spilling build, a grace reload under pressure) left such chunks, and their rows were lost: the bench-runner spill_equivalence and q21-shaped memory_bound tests returned fewer rows (option C, ab45aa5, found by bisecting #749's CI failure). The chunks past the block now stay, after its tail. A test cuts three chunks of 10,000 rows at once and checks every row is kept. Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
…r join The rule read every inner join under every consumer as a candidate (output resolution, a repeatable walk, the path checks) and only then refused those with no non-inner join on their path: a plan of inner joins paid for all of it and moved nothing (CodSpeed on #749: join_chain_12 +23-30%, optimize_tpch_22 +11%). walk now carries whether its path holds a non-inner join and collects candidates only then; what moves is the same. Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
|
CI on test / diff-coverage: CodSpeed: the optimizer regressions ( perf-1's S5 timed runs are on hold until they re-run on this head ( |
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
Conflicts: the optimizer config registry and imports (latePayloadJoin beside #734's groupKeyCode keys), the exec and optimizer CLAUDE.md maps; both sides kept. join/build.rs merged cleanly: the partition reservations S5 trues down at the seal now travel with #734's Level0 tables. The LatePayloadJoin tests switch the group-key code (D2) off: without a session configuration `Options::default()` runs it, and it coded the dimension keys those tests move as payload. Sessions run D2 only with krishiv.optimizer.groupKeyCode on (off by default). Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com>
|
Merged main |
Signed-off-by: gopal tathe <44522021+techgopal@users.noreply.github.com> # Conflicts: # crates/krishiv-common/src/config/tests.rs
|
main merged in for the CI disk fix (#776); lane: git pull before your next push |
P3 plan row PAR, combined PR (2): S5, the TPC-H q21 parity lever (gap ledger row q21; brief
ops/briefs/2026-11-13-parity-1x.md). Built by opus-1 and opus-2, handed to perf-2 at the end of W0, merged with main and finished here.What it does
id & maskof blockid >> 14, with no chunk table and no search. Build chunks are copied into blocks once, as they come. Finalize slices whole blocks in place and copies only the tails. Each partition's reservation is then trued down to what its arena holds (RowArena::held_bytes).krishiv.optimizer.latePayloadJoin, default on). A dimension's payload joins above the semi and anti joins that never read it (q21'ss_name).Measured
q21 SF1000, the lever's line (perf-1, 2026-11-13, laptop alone, on opus-2's stack
4721175ce):reproduceagainst a fresh DuckDB: 277.0 s = 1.42x (gate run 473.8 s, 2.36x);results/20261113T100754Z_reproduce_tpch_sf1000.json.Small-build finalize. perf-1's 11-09 SF10 ABBA had found every small build slower: 9 ms became 49-87 ms. Cause, measured with an instrumented build: build chunks of about 20 rows (q10 op 7: 29,359 chunks for 573K rows), walked twice by finalize's accounting before any partition started. Funding waits were zero. Finalize wall after the fix, SF10 warm, 12 threads:
Still owed, booked with perf-1 (
ops/queue-opus-b1.md, B = this branch's head):latePayloadJoinon and off (line: <= 0.95x).Draft until (a) is in.
Review
ops/reviews/2026-11-13-perf-2-s5-stack-diff.md(krishiv-internal): CHANGES_REQUIRED, 1 Important. The Important was the 5/4 sizing rounding down: 1,639 keys doubled the directory with nothing reserved for it. Fixed in 47eb83c, with a test that fails on the old sizing.ops/reviews/2026-11-14-perf-2-s5-stack-rereview.md: APPROVED. Its M6 (cut before merge) and M1 (akrishiv.deterministicOrderdoc sentence) are fixed in the last commit.probe_table_nsA/B for the 5/4 index.Verification
8739b40c8, krishiv-exec + krishiv-common 1260/1260 at2dd5875e3(local); krishiv-common + krishiv-optimizer 341/341 after the merge.-D warnings) and deny clean (pre-commit).